package net.mingsoft.flk.gmcryptojni;

import lombok.extern.slf4j.Slf4j;

import java.util.Base64;

@Slf4j
public class GMCryptoUtil {
    private GMCrypto GMCRYPTO;
    private GMCrypto.GmKey GMKEY;
    private boolean GMCRYPTO_READY;
    private boolean IS_SAVE_TO_LOCAL;
    private String KMS_IP;//密钥管理服务器ip地址；由部署人员告知
    private int KMS_PORT;//密钥管理服务器服务端口；由部署人员告知
    private String SYNC_SIGN_IP;//协同签名服务器ip地址；由部署人员告知
    private int SYNC_SIGN_PORT;//协同签名服务器服务端口；由部署人员告知
    private String SDK_DIR;//sdk用于存放私钥分量、kms证书、错误日志的文件夹；需要提前创建好
    private String DEVICE_SN;//用于标识sdk所运行的设备，可自定义，应用内全局唯一即可，长度不超过64字节；sdk创建的私钥分量与协同签名服务器的私钥分量通过DEVICE_SN来关联

    /**
     * 公私钥对生成凭证，由SDK上传到协同签名服务器并委托应用服务器校验，生成、下发、上传、校验流程详情见配套文档；
     * SDK在运行环境初始化时，如果在SDK_DIR没有检测到私钥分量文件，则向协同签名服务器发起请求创建公私钥对。协同签名服务器在响应公私钥对的创建时需要通过AUTH_CODE校验请求合法性。
     */
    private String AUTH_CODE;

    private String ORGANIZATION_ID;//机构id，与密码服务平台对接时填写，用于标识请求者身份；由密码服务平台预分配
    private String APP_ID;//应用id，与密码服务平台对接时填写，用于标识请求者身份；由密码服务平台预分配
    private String API_KEY;//应用身份凭证，与密码服务平台对接时填写，用于标识请求者身份；由密码服务平台预分配

    private static class SingletonClassInstance{
        private static final GMCryptoUtil instance=new GMCryptoUtil();
    }

    private GMCryptoUtil(){
        GMCRYPTO = new GMCrypto();
    }

    public static GMCryptoUtil getInstance(){
        return SingletonClassInstance.instance;
    }


    /**
     * 对字符串形式的plainMsg进行密码运算，返回密文字符串
     * @param jcrypto
     * @param keyId
     * @param iv 初始向量
     * @param plainMsg 明文字符串
     * @return 密文字符串
     */
    private String  sm4Encrypt(GMCrypto jcrypto, int keyId,String iv,String plainMsg){

        byte[] plainBytes = plainMsg.getBytes();

        //加密
        GMCrypto.Gmtext cipherData = jcrypto.sm4EncryptByKEK(keyId, GMCrypto.GmAlgID.CBC.value, iv,plainBytes);
        if(cipherData.errorCode != 0)
        {
            log.info("sm4EncryptByKEK failed ret= {}", cipherData.errorCode);
            return null;
        }

        String cipherStr = Base64.getEncoder().encodeToString(cipherData.text);

        return cipherStr;
    }

    private String  sm4Decrypt(GMCrypto jcrypto, int keyId,String iv,String cipherMsg){

        byte[] cipherBytes = Base64.getDecoder().decode(cipherMsg.getBytes());
        if (cipherBytes == null){
            return null;
        }
        GMCrypto.Gmtext cipherData  = jcrypto.sm4DecryptByKEK(keyId,GMCrypto.GmAlgID.CBC.value,iv,cipherBytes);
        if(cipherData.errorCode != 0)
        {
            log.info("smDecryptByKEK failed ret = {}", cipherData.errorCode);
            return null;
        }
        return new String(cipherData.text);

    }

    private String sm4Mac(GMCrypto jcrypto,int keyId,String iv,String msg){
        GMCrypto.Gmtext macData  = jcrypto.sm4Mac(keyId,iv,msg.getBytes());
        log.info("sm4Mac1  {}{}", macData, macData.errorCode);
        if (macData.errorCode != 0) {
            log.info("sm4Mac failed ret = {}", macData.errorCode);
            return null;
        }
        String macStr = Base64.getEncoder().encodeToString(macData.text);
        return macStr;
    }



    //初始化
    public  synchronized int initModule(String kmsIp,
                                        int kmsPort,String syncSignIp,int syncSignPort,
                                        String sdkDir,String DeviceSn,String authCode) {
        return initModule(kmsIp,kmsPort,syncSignIp,syncSignPort,sdkDir,DeviceSn,authCode,null,null,null);
    }

    //初始化
    public  synchronized int initModule(String kmsIp,
                                        int kmsPort,String syncSignIp,int syncSignPort,
                                        String sdkDir,String DeviceSn,String authCode,
                                        String organzationId,String appId,String apiKey){
        if (System.getProperty("os.name").toLowerCase().indexOf("win") != -1){
            IS_SAVE_TO_LOCAL = false;
        }else{
            IS_SAVE_TO_LOCAL = true;
        }
        GMCRYPTO_READY = false;
        KMS_IP = kmsIp;
        KMS_PORT = kmsPort;
        SYNC_SIGN_IP = syncSignIp;
        SYNC_SIGN_PORT = syncSignPort;
        SDK_DIR = sdkDir;
        DEVICE_SN = DeviceSn;
        AUTH_CODE = authCode;
        ORGANIZATION_ID = organzationId;
        APP_ID = appId;
        API_KEY = apiKey;

        GMKEY = null;

        GMCrypto.GmKMSModule jmodule = new GMCrypto.GmKMSModule();
        //调用加密接口、签名接口时初始化
        jmodule.deviceSn = DeviceSn;
        jmodule.kmsServerAddr = kmsIp;
        jmodule.kmsPort = kmsPort;
        jmodule.syncServerAddr = syncSignIp;
        jmodule.syncPort = syncSignPort;
        jmodule.serverKeyPath = sdkDir;
        jmodule.configPath = sdkDir;
        jmodule.logPath = sdkDir;
        jmodule.authCode = authCode;
        if(IS_SAVE_TO_LOCAL){
            jmodule.setDeviceType(1);
        }else{
            jmodule.setDeviceType(11);
        }

        int ret = GMCrypto.initGMKMSModule(jmodule);
        if(ret != 0)
        {
            log.info("initGMKMSModule failed ret = {}", ret);
        }else{
            GMCRYPTO_READY = true;
        }
        return ret;
    }

    public synchronized void uninitModule(){
        GMKEY = null;
        GMCRYPTO_READY = false;
        GMCrypto.uninitGMKMSModule();
    }

    //获取密钥；
    //没有初始化时先初始化；
    //密钥没有获取到的话，先获取kms最新密钥
    private GMCrypto.GmKey  getKey(String keyVersion,String deriveKeyId){
        GMCrypto.GmKey gmKey = new GMCrypto.GmKey();
        GMCRYPTO_READY = false;
        //首先初始化,初始化时与服务器建立连接，获取数字签名。数字签名用于获取对称密钥。
        if (GMCRYPTO_READY == false){

            int ret = initModule(KMS_IP,
                    KMS_PORT,SYNC_SIGN_IP,SYNC_SIGN_PORT,
                    SDK_DIR,DEVICE_SN,AUTH_CODE,
                    ORGANIZATION_ID,APP_ID,API_KEY);
            if (ret == 0){
                GMCRYPTO_READY = true;
            }
        }

        if (GMCRYPTO_READY == false){
            gmKey.keyId = -1;
            return gmKey;
        }

        gmKey = GMCRYPTO.getKey(keyVersion, deriveKeyId);//第一个参数传null标识获取KMS最新版本密钥，第二个参数传null表示不需要派生密钥
        if(gmKey.keyId < 0)
        {
            log.info("getKEK failed ret = {}", gmKey.keyId);
            gmKey.keyId = -1;
            return gmKey;
        }

        return gmKey;
    }

    //使用最新版本密钥加密数据
    public  String encryptMsg(String plainMsg, String iv) {
        if (plainMsg == null || plainMsg.length() == 0 || iv == null || iv.length() != 16){
            log.info("encryptMsg/2: get key invoked  ==> plainMsg: {}  iv: {}", plainMsg, iv);
            return null;
        }
        // KMS根据最新版本的密钥与派生id运算得到新的密钥.
        if (GMKEY == null || GMKEY.keyId <= 0) {
            //获取kms最新密钥；kms密钥更新策略由kms管理后台进行配置.
            GMKEY = getKey(null,null);
            log.info("encryptMsg/2: get key invoked");
        }
        // 如果已经获取到密钥
        if (GMKEY.keyId > 0) {
            String cipherMsg =  sm4Encrypt(GMCRYPTO, GMKEY.keyId,iv,plainMsg);
            String ivBase64 = Base64.getEncoder().encodeToString(iv.getBytes());
            return GMKEY.keyVersion + "&" + cipherMsg + "&" + ivBase64;
        } else { // 密钥获取失败，返回 null
            log.info("encryptMsg/2: GMKEY.keyId={} ==>  plainMsg: {}  iv: {}", GMKEY.keyId, plainMsg, iv);
            return null;
        }
    } // encryptMsg()

    //解密数据
    public  String decryptMsg(String cipherMsg) {
        String[] cipherParts = cipherMsg.split("&");
        if (cipherParts.length != 3){
            log.info("decryptMsg/1: 密文格式不正确，应包含密钥版本号、密文、iv四部分 ==> decryptMsg: {}  cipherMsg: {}",
                cipherParts.length, cipherMsg);
            return null;
        }
        log.info("decryptMsg/1: {}  cipherMsg: {}", cipherParts.length, cipherMsg);

        final String keyVersion = cipherParts[0];
        //获取密文对应版本密钥；如果对应密钥已经获取过，则使用本地密钥；否则向kms发起网络请获取对应版本密钥。
        final GMCrypto.GmKey gmKey = getKey(keyVersion,null);
        if (gmKey.keyId > 0) {
            final String ivReceived = new String(Base64.getDecoder().decode(cipherParts[2]));
            if (ivReceived.length() != 16) {
                log.info("decryptMsg/1: 密文格式不正确，收到的iv长度应为16  ==> cipherMsg: {}", cipherMsg);
                return null;
            }
            final String plainMsg =  sm4Decrypt(GMCRYPTO, gmKey.keyId,ivReceived,cipherParts[1]);
            return plainMsg;
        } else { // 密钥获取失败，返回null
            return null;
        }
    } // decryptMsg()


    //数字签名
    public String sm2Sign(String plainMsg){
        int ret = -1;

        /*使用非对称加密算法接口*/
        GMCrypto.ECCrefPublicKey publicKey = new GMCrypto.ECCrefPublicKey();
        //获取本设备公钥；若本地第一次请求，则与协同服务器交互，生成本设备公私钥对，本地加密存放公钥与私钥分量1，服务器存放公钥与私钥分量2；
        ret = GMCRYPTO.getSyncKeyPairs(publicKey);
        if (ret < 0){
            log.info("getSyncKeyPairs faield,ret is: {}", ret);
            return null;
        }

        byte[] plainBytesCopy = new byte[plainMsg.getBytes().length];
        System.arraycopy(plainMsg.getBytes(),0,plainBytesCopy,0,plainMsg.getBytes().length);//copy

        //计算hash值,为数字签名做准备
        GMCrypto.Gmtext hashData = GMCRYPTO.sm3Hash2Sign(plainBytesCopy,publicKey);
        if (hashData.errorCode != 0){
            log.info("sm3Hash2Sign failed,ret: {}", hashData.errorCode);
            return null;
        }

        //与协同签名服务交互，生成完整数字签名。
        GMCrypto.ECCSignature signature = new GMCrypto.ECCSignature();
        ret = GMCRYPTO.sm2Sign(hashData.text,signature);
        if (ret != 0){
            log.info("sm2Sign failed,ret: {}", ret);
            return null;
        }
        return plainMsg+"&"+signature.toString()+"&"+DEVICE_SN;
    }

    //验证数字签名
    public int verify(String msg){
        String[] signatureParts = msg.split("&");
        if (signatureParts.length != 3){
            log.info("格式不正确，应包含明文、签名值、DevceSN三部分");
            return -1;
        }
        String plainMsg = signatureParts[0];
        String signature = signatureParts[1];
        String deviceSN = signatureParts[2];

        //验签接口由协同签名服务器提供
        //TODO:想协同签名服务器发送http请求，json请求体{plain:"MyPlainMsg",signature:"MySignature",DeviceSN:"MyDeviceSN"}
        int ret  = postSign(plainMsg,signature,deviceSN);

        if(ret == 0){

        }
        return 0;
    }

    public int postSign(String plainMsg,String signature,String deviceSN){
        //TODO:发起网络请求
        return 0;
    }


    private String hmacSm3(GMCrypto jcrypto,int keyId,String msg){
        GMCrypto.Gmtext macData  = jcrypto.hmacSm3(keyId,msg.getBytes());
        log.info("hmacSm3  {}{}", macData, macData.errorCode);
        if (macData.errorCode != 0) {
            log.info("sm4Mac failed ret = {}", macData.errorCode);
            return null;
        }
        String macStr = Base64.getEncoder().encodeToString(macData.text);
        return macStr;
    }

    //计算校验码
    public String hmacSm3Msg(String plainMsg){
        if (plainMsg == null || plainMsg.length() == 0 ){
            log.info("encryptMsg -> get key invoked");
            return null;
        }
        //KMS根据最新版本的密钥与派生id运算得到新的密钥.
        if (GMKEY == null || GMKEY.keyId<=0){
            //获取kms最新密钥；kms密钥更新策略由kms管理后台进行配置.
            GMKEY = getKey(null,null);
            log.info("encryptMsg -> get key invoked");
        }
        if (GMKEY.keyId > 0){//如果已经获取到密钥
            String hmac = hmacSm3(GMCRYPTO, GMKEY.keyId,plainMsg);
            return GMKEY.keyVersion+"&"+hmac;
        }else{//密钥获取失败，返回null
            log.info("encryptMsg GMKEY.keyId null ");
            return null;
        }
    }

    //计算校验码
    public boolean verifyHmacSm3Msg(String plainMsg,String hmacMsg){
        if (hmacMsg == null || hmacMsg.length() == 0 ){
            log.info("encryptMsg -> get key invoked");
            return false;
        }

        String[] hmacParts = hmacMsg.split("&");
        log.info("decryptMsg {}", hmacParts.length);
        if (hmacParts.length != 2){
            log.info("密文格式不正确，应包含密钥版本号、消息验证码");
            return false;
        }
        String keyVersion = hmacParts[0];
        //获取密文对应版本密钥；如果对应密钥已经获取过，则使用本地密钥；否则向kms发起网络请获取对应版本密钥。
        GMCrypto.GmKey gmKey = getKey(keyVersion,null);
        if (gmKey.keyId > 0){
            String hmac1 = hmacSm3(GMCRYPTO, GMKEY.keyId,plainMsg);
            String hmac2 = hmacParts[2];
            if (!hmac1.equalsIgnoreCase(hmac2)){
                log.info("数据完整性校验未通过");
                return false;
            }
            return true;
        }else{//密钥获取失败，返回false
            return false;
        }

    }



}
